import os
import json
import logging
import subprocess
import random
import socket
import requests
import shutil
from typing import Dict, List, Any, Optional
from datetime import datetime, timezone
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
from flask import current_app

# OSS支持（可选）
try:
    import oss2
    OSS_AVAILABLE = True
except ImportError:
    OSS_AVAILABLE = False

logger = logging.getLogger(__name__)

class ExperimentService:
    """统一的实验服务类 - 整合参考项目功能"""
    
    def __init__(self):
        """初始化实验服务"""
        self.active_ports = set()
        self._load_active_ports()
        self._init_oss_client()
        self._init_jinja_env()
    
    def _init_oss_client(self):
        """初始化OSS客户端（如果可用）"""
        self.oss_client = None
        if (OSS_AVAILABLE and 
            current_app.config.get('USE_OSS') and 
            current_app.config.get('OSS_ACCESS_KEY_ID') and 
            current_app.config.get('OSS_ACCESS_KEY_SECRET')):
            try:
                auth = oss2.AuthV4(
                    current_app.config['OSS_ACCESS_KEY_ID'],
                    current_app.config['OSS_ACCESS_KEY_SECRET']
                )
                self.oss_client = oss2.Bucket(
                    auth,
                    current_app.config['OSS_ENDPOINT'],
                    current_app.config['OSS_BUCKET_NAME'],
                    region=current_app.config['OSS_REGION']
                )
                logger.info("OSS客户端初始化成功")
            except Exception as e:
                logger.error(f"OSS客户端初始化失败: {e}")
    
    def _init_jinja_env(self):
        """初始化Jinja2模板环境"""
        template_dirs = [
            "templates",
            os.path.join(os.path.dirname(__file__), "..", "..", "templates"),
            "/home/mobox/Documents/mobox_dashboard/backend/templates"
        ]
        
        template_dir = None
        for dir_path in template_dirs:
            if os.path.exists(dir_path):
                template_dir = dir_path
                break
        
        if template_dir:
            self.jinja_env = Environment(
                loader=FileSystemLoader(template_dir),
                autoescape=select_autoescape(['yml', 'yaml'])
            )
            logger.info(f"模板环境初始化成功: {template_dir}")
        else:
            self.jinja_env = None
            logger.warning("未找到模板目录")
    
    def _load_active_ports(self):
        """加载当前活跃的端口"""
        try:
            result = subprocess.run(
                ["docker", "ps", "--format", "{{.Ports}}"],
                capture_output=True, text=True, check=True
            )
            
            ports = set()
            for line in result.stdout.splitlines():
                if "->" in line:
                    port_mappings = line.split(",")
                    for mapping in port_mappings:
                        if "->" in mapping:
                            host_port = mapping.split("->")[0].split(":")[-1]
                            try:
                                ports.add(int(host_port))
                            except ValueError:
                                continue
            
            self.active_ports = ports
            logger.info(f"已加载活跃端口: {len(self.active_ports)} 个")
        except Exception as e:
            logger.error(f"加载活跃端口失败: {e}")
            self.active_ports = set()
    
    def get_free_port(self) -> int:
        """获取空闲端口"""
        port_start = current_app.config.get('PORT_RANGE_START', 8001)
        port_end = current_app.config.get('PORT_RANGE_END', 9000)
        port_range = range(port_start, port_end + 1)
        available_ports = list(set(port_range) - self.active_ports)
        
        if not available_ports:
            raise ValueError("没有可用端口")
        
        port = random.choice(available_ports)
        
        # 双重检查端口是否真的可用
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            if s.connect_ex(('localhost', port)) == 0:
                return self.get_free_port()  # 递归查找
        
        self.active_ports.add(port)
        return port
    
    def create_or_get_student_workspace(self, user_id: str, course_name: str) -> str:
        """
        为学生创建或获取课程工作区。
        如果工作区已存在，则直接返回路径。
        如果不存在，则从课程模板库中拷贝。
        """
        # 从配置中获取模板库和学生工作区的根路径
        course_template_root = current_app.config.get('COURSE_BASE_PATH')
        student_workspace_root = current_app.config.get('STUDENT_WORKSPACE_PATH')
        
        # 定义源路径 (课程模板) 和目标路径 (学生个人工作区)
        source_course_dir = os.path.join(course_template_root, course_name)
        destination_user_dir = os.path.join(student_workspace_root, user_id, course_name)

        # 检查课程模板是否存在
        if not os.path.isdir(source_course_dir):
            logger.error(f"课程模板不存在: {source_course_dir}")
            raise FileNotFoundError(f"无法找到课程模板 '{course_name}'")

        # 核心逻辑：如果学生的工作区目录不存在，则从模板库拷贝
        if not os.path.exists(destination_user_dir):
            logger.info(f"为用户 '{user_id}' 创建课程 '{course_name}' 的工作区...")
            logger.info(f"从 '{source_course_dir}' 拷贝到 '{destination_user_dir}'")
            try:
                # 使用 shutil.copytree 递归拷贝整个目录
                shutil.copytree(source_course_dir, destination_user_dir)
                logger.info("拷贝完成！")
            except Exception as e:
                logger.error(f"拷贝课程目录失败: {e}")
                raise IOError(f"无法为学生创建课程工作区: {e}")
        else:
            logger.info(f"用户 '{user_id}' 的课程 '{course_name}' 工作区已存在，直接使用。")

        return destination_user_dir

    def generate_experiment_url(self, user_id: str, experiment_id: str, course_name: str, course_type: str = 'default') -> dict:
        """生成实验资源URL - 直接指向Docker容器内的具体实验目录"""
        try:
            # 验证课程是否存在（在Docker容器内）
            course_template_root = current_app.config.get('COURSE_BASE_PATH')
            course_path = os.path.join(course_template_root, course_name)
            experiment_path = os.path.join(course_path, experiment_id)
            
            # 从配置中获取端口映射
            port_mapping = current_app.config.get('EXPERIMENT_PORTS', {})
            # 根据课程类型获取端口，如果类型不存在，则使用默认端口
            port = port_mapping.get(course_type, port_mapping.get('default', 8080))

            # 生成访问URL，直接指向Docker容器内的具体实验目录
            host_ip = current_app.config.get('HOST_IP', 'localhost')
            service_url = f"http://{host_ip}:{port}/?folder={experiment_path}"
            
            return {
                'status_code': 200,
                'data': {
                    'url': service_url,
                    'last_time': datetime.now(timezone.utc).isoformat(),
                    'experiment_path': experiment_path
                },
                'message': 'Resource URL generated successfully'
            }
        except Exception as e:
            logger.error(f"生成实验URL失败: {e}")
            return {
                'status_code': 404,
                'data': None,
                'message': 'Experiment resource not found'
            }
    
    def start_docker_experiment(self, user_id: str, experiment_id: str, course_type: str) -> dict:
        """启动Docker实验环境"""
        try:
            # 确保实验目录
            user_dir = self.ensure_experiment_directory(user_id, course_type, experiment_id)
            
            # 分配端口
            port = self.get_free_port()
            
            # 生成Docker Compose配置
            compose_file = self._generate_docker_compose(
                user_id, experiment_id, course_type, port, user_dir
            )
            
            # 启动服务
            service_name = f"{user_id}_{course_type}_{experiment_id}"
            result = subprocess.run(
                ["docker-compose", "-f", compose_file, "-p", service_name, "up", "-d"],
                capture_output=True, text=True, check=True
            )
            
            host_ip = current_app.config.get('HOST_IP', 'localhost')
            service_url = f"http://{host_ip}:{port}/"
            
            logger.info(f"Docker实验启动成功: {service_name}")
            
            return {
                'status_code': 200,
                'data': {
                    'service_url': service_url,
                    'service_name': service_name,
                    'port': port
                },
                'message': 'Docker实验环境启动成功'
            }
            
        except Exception as e:
            logger.error(f"启动Docker实验失败: {e}")
            return {
                'status_code': 500,
                'data': None,
                'message': f'启动失败: {str(e)}'
            }
    
    def _generate_docker_compose(self, user_id: str, experiment_id: str, 
                                course_type: str, port: int, user_dir: str) -> str:
        """生成Docker Compose文件"""
        service_name = f"{user_id}_{course_type}_{experiment_id}"
        
        if self.jinja_env:
            try:
                template = self.jinja_env.get_template("docker-compose.yml.j2")
                context = {
                    "service_name": service_name,
                    "user_id": user_id,
                    "experiment_id": experiment_id,
                    "course_name": course_type,
                    "port": port,
                    "user_dir": user_dir,
                    "course_files_path": current_app.config.get('COURSE_BASE_PATH'),
                    "host_ip": current_app.config.get('HOST_IP')
                }
                content = template.render(**context)
            except Exception as e:
                logger.error(f"模板渲染失败: {e}")
                content = self._default_compose_content(service_name, port, user_dir)
        else:
            content = self._default_compose_content(service_name, port, user_dir)
        
        # 保存配置文件
        output_dir = os.path.join(user_dir, "docker_compose")
        os.makedirs(output_dir, exist_ok=True)
        compose_file = os.path.join(output_dir, f"{service_name}.yml")
        
        with open(compose_file, 'w') as f:
            f.write(content)
        
        return compose_file
    
    def _default_compose_content(self, service_name: str, port: int, user_dir: str) -> str:
        """默认Docker Compose内容"""
        course_path = current_app.config.get('COURSE_BASE_PATH', '/home/mobox/course')
        return f"""version: '3.8'
services:
  {service_name}:
    image: jupyter/base-notebook:latest
    container_name: {service_name}
    ports:
      - "{port}:8888"
    volumes:
      - "{user_dir}:/home/jovyan/work"
      - "{course_path}:/home/jovyan/course"
    environment:
      - JUPYTER_ENABLE_LAB=yes
      - JUPYTER_TOKEN=""
    command: start-notebook.sh --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.ip='0.0.0.0'
    restart: unless-stopped
"""
    
    def stop_docker_experiment(self, user_id: str, experiment_id: str, course_type: str) -> dict:
        """停止Docker实验环境"""
        try:
            service_name = f"{user_id}_{course_type}_{experiment_id}"
            
            result = subprocess.run(
                ["docker-compose", "-p", service_name, "down"],
                capture_output=True, text=True, check=True
            )
            
            # 重新加载活跃端口
            self._load_active_ports()
            
            logger.info(f"Docker实验停止成功: {service_name}")
            
            return {
                'status_code': 200,
                'data': {'stopped': True},
                'message': 'Docker实验环境停止成功'
            }
            
        except Exception as e:
            logger.error(f"停止Docker实验失败: {e}")
            return {
                'status_code': 500,
                'data': None,
                'message': f'停止失败: {str(e)}'
            }
    
    def list_user_experiments(self, user_id: str) -> dict:
        """列出用户的实验环境"""
        try:
            result = subprocess.run(
                ["docker", "ps", "-a", "--format", "json"],
                capture_output=True, text=True, check=True
            )
            
            experiments = []
            for line in result.stdout.strip().split('\n'):
                if line:
                    try:
                        container_info = json.loads(line)
                        container_name = container_info.get('Names', '')
                        
                        if container_name.startswith(f"{user_id}_"):
                            parts = container_name.split('_')
                            if len(parts) >= 3:
                                course_type = '_'.join(parts[1:-1])
                                experiment_id = parts[-1]
                                
                                experiments.append({
                                    'experiment_id': experiment_id,
                                    'course_type': course_type,
                                    'status': container_info.get('State', 'unknown'),
                                    'ports': container_info.get('Ports', ''),
                                    'created': container_info.get('CreatedAt', ''),
                                    'container_name': container_name
                                })
                    except json.JSONDecodeError:
                        continue
            
            return {
                'status_code': 200,
                'data': experiments,
                'message': f'成功获取用户 {user_id} 的实验列表'
            }
            
        except Exception as e:
            logger.error(f"列出用户实验失败: {e}")
            return {
                'status_code': 500,
                'data': [],
                'message': f'获取失败: {str(e)}'
            }
    
    def get_all_experiments(self) -> List[Dict[str, Any]]:
        """获取所有用户的实验环境"""
        try:
            result = subprocess.run(
                ["docker", "ps", "-a", "--format", "json"],
                capture_output=True, text=True, check=True
            )
            
            experiments = []
            for line in result.stdout.strip().split('\n'):
                if line:
                    try:
                        container_info = json.loads(line)
                        container_name = container_info.get('Names', '')
                        
                        # 检查是否是实验容器（格式：user_coursetype_experimentid）
                        if '_' in container_name and not container_name.startswith('mobox-'):
                            parts = container_name.split('_')
                            if len(parts) >= 3:
                                user_id = parts[0]
                                course_type = '_'.join(parts[1:-1])
                                experiment_id = parts[-1]
                                
                                # 从Ports字段提取端口信息
                                ports_info = container_info.get('Ports', '')
                                url = None
                                if '->' in ports_info:
                                    # 提取映射的端口，例如 "0.0.0.0:8080->8080/tcp"
                                    import re
                                    port_match = re.search(r'(\d+)->', ports_info)
                                    if port_match:
                                        port = port_match.group(1)
                                        url = f"http://localhost:{port}"
                                
                                experiments.append({
                                    'experiment_id': experiment_id,
                                    'course_name': course_type,
                                    'user_id': user_id,
                                    'status': container_info.get('State', 'unknown'),
                                    'created_time': container_info.get('CreatedAt', ''),
                                    'url': url,
                                    'container_name': container_name
                                })
                    except json.JSONDecodeError:
                        continue
            
            return experiments
            
        except Exception as e:
            logger.error(f"获取所有实验失败: {e}")
            return []

# 全局服务实例
experiment_service = None

def get_experiment_service():
    """获取实验服务实例（单例模式）"""
    global experiment_service
    if experiment_service is None:
        experiment_service = ExperimentService()
    return experiment_service 